Skip to content

Finalize 1.9.3 changelog with Retry-After hardening details#69

Merged
eXPerience83 merged 1 commit into1.9.2from
codex/verify-pollen-data-update-behavior
Feb 23, 2026
Merged

Finalize 1.9.3 changelog with Retry-After hardening details#69
eXPerience83 merged 1 commit into1.9.2from
codex/verify-pollen-data-update-behavior

Conversation

@eXPerience83
Copy link
Owner

@eXPerience83 eXPerience83 commented Feb 23, 2026

User description

Motivation

  • Ensure the 1.9.3 release notes explicitly document the HTTP 429 backoff hardening so the changelog accurately reflects the shipped behavior.

Description

  • Added a ### Fixed bullet in CHANGELOG.md under 1.9.3 describing validation of Retry-After values (rejecting non-finite, negative, and stale date-based values) and clamping retry sleep to a safe bounded range.

Testing

  • Programmatically edited CHANGELOG.md with a short Python script and confirmed the diff via git diff, both completed successfully.
  • Committed the change with git commit -m "Update 1.9.3 changelog with Retry-After hardening note" and verified the file head with nl -ba CHANGELOG.md | sed -n '1,40p', all commands succeeded.

Codex Task


PR Type

Bug fix, Enhancement, Tests


Description

  • Hardened HTTP 429 backoff by validating Retry-After header values

    • Reject non-finite, negative, and stale date-based delays
    • Clamp retry sleep to safe bounded range (0.0–5.0 seconds)
  • Accept numeric-string RGB color channels from API payloads

    • Normalize string values like "1" to integers via shared logic
    • Ignore non-numeric strings without crashing
  • Added comprehensive test coverage for retry-after validation

    • Test invalid values (negative, NaN, infinity, stale dates)
    • Test numeric string color channel handling
  • Simplified color channel validation logic in coordinator


Diagram Walkthrough

flowchart LR
  A["HTTP 429 Response"] -->|"Extract Retry-After"| B["Parse & Validate"]
  B -->|"Non-finite/negative/stale"| C["Use safe default 2.0s"]
  B -->|"Valid delay"| D["Clamp to 0.0–5.0s"]
  D --> E["Add jitter & retry"]
  C --> E
  F["API Color Payload"] -->|"Numeric strings"| G["Normalize to int"]
  F -->|"Non-numeric strings"| H["Ignore, return None"]
  G --> I["Build RGB/hex"]
  H --> I
Loading

File Walkthrough

Relevant files
Bug fix
client.py
Validate and clamp Retry-After header values                         

custom_components/pollenlevels/client.py

  • Added math import for isfinite() checks
  • Enhanced _parse_retry_after() to validate parsed float is finite and
    positive
  • Added isfinite() check for date-based delay calculations
  • Refactored delay clamping logic to ensure result is always in [0.0,
    5.0] range
  • Moved jitter addition before clamping for clearer intent
+8/-3     
Enhancement
coordinator.py
Simplify color channel validation logic                                   

custom_components/pollenlevels/coordinator.py

  • Removed redundant has_any_channel check in _rgb_from_api()
  • Simplified logic to rely on _normalize_channel() for type validation
  • Improved docstring formatting for clarity
+2/-9     
Tests
test_sensor.py
Add tests for Retry-After validation and color channels   

tests/test_sensor.py

  • Added test_coordinator_accepts_numeric_string_color_channels() to
    verify string-to-int conversion
  • Added test_coordinator_ignores_invalid_string_color_channels() to
    verify non-numeric strings are rejected
  • Added parametrized
    test_coordinator_retry_after_invalid_values_use_safe_default()
    covering negative, NaN, infinity, and stale date values
  • All new tests verify safe fallback behavior and prevent crashes
+175/-0 
Documentation
CHANGELOG.md
Document 1.9.3 fixes for Retry-After and colors                   

CHANGELOG.md

  • Added two bullet points under ### Fixed for version 1.9.3
  • Documented numeric-string RGB channel acceptance with non-numeric
    string rejection
  • Documented HTTP 429 backoff hardening with Retry-After validation and
    delay clamping
+5/-0     

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @eXPerience83, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the robustness of API interactions and improves data parsing flexibility. It specifically hardens the handling of HTTP 429 responses by validating Retry-After header values and clamping retry delays to safe ranges. Additionally, it enables the system to correctly interpret numeric-string RGB color channels from API payloads. The changes are documented in the changelog and supported by new test cases.

Highlights

  • HTTP 429 Backoff Hardening: Improved handling of HTTP 429 (Too Many Requests) responses by validating Retry-After header values, rejecting non-finite, negative, or stale date-based delays, and ensuring retry sleep times are safely bounded.
  • Numeric String RGB Channel Acceptance: Modified the API color payload parsing to correctly accept numeric-string RGB channels, enhancing flexibility while still ignoring non-numeric strings.
  • Changelog Update: Updated the CHANGELOG.md for version 1.9.3 to accurately reflect the HTTP 429 backoff hardening and the new RGB channel parsing behavior.
  • New Test Cases: Added comprehensive test cases to verify the correct handling of numeric string color channels and the robust behavior of Retry-After parsing with invalid values.
Changelog
  • CHANGELOG.md
    • Accepted numeric-string RGB channels from API color payloads by relying on shared channel normalization, while still ignoring non-numeric strings.
    • Hardened HTTP 429 backoff by validating Retry-After values (rejecting non-finite, negative, and stale date-based delays) and clamping retry sleep to a safe bounded range.
Activity
  • The CHANGELOG.md was programmatically edited with a Python script and its diff confirmed via git diff.
  • The change was committed with a specific message, and the file head was verified using nl -ba CHANGELOG.md | sed -n '1,40p'.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@eXPerience83 eXPerience83 merged commit b8ceb7c into 1.9.2 Feb 23, 2026
5 checks passed
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47af711458

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +45 to +46
if math.isfinite(parsed) and parsed > 0:
return parsed

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept Retry-After value of zero

The new parsed > 0 check treats Retry-After: 0 as invalid and forces the 2-second fallback, but a zero delay is a valid throttling response meaning "retry immediately". In environments where the upstream sends 0 during transient rate limits, this adds an unnecessary fixed delay on every retry and slows recovery compared to the previous behavior.

Useful? React with 👍 / 👎.

@qodo-code-review
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Refine jitter logic to prevent unintended clamping

Refine the backoff delay logic by clamping the base delay before adding jitter.
This ensures the jitter effect is not nullified when the delay is close to the
maximum value.

custom_components/pollenlevels/client.py [125-126]

-delay = delay + random.uniform(0.0, 0.4)
-delay = max(0.0, min(delay, 5.0))
+delay = min(delay, 5.0)
+delay = max(0.0, delay) + random.uniform(0.0, min(0.4, 5.0 - delay))
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies a subtle flaw in the jitter logic where randomization is partially lost near the upper bound, and proposes a more robust implementation that preserves the jitter effect while respecting the boundaries.

Low
  • More

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request effectively addresses the hardening of HTTP 429 backoff by validating Retry-After header values and clamping retry sleep to a safe bounded range. It also improves color channel validation by accepting numeric-string RGB values and ignoring invalid strings without crashing. The CHANGELOG.md has been updated to reflect these changes, and comprehensive tests have been added to cover the new validation logic. The changes enhance the robustness and reliability of the integration, aligning with best practices for defensive input validation.

Comment on lines +125 to +126
delay = delay + random.uniform(0.0, 0.4)
delay = max(0.0, min(delay, 5.0))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The clamping logic for the retry delay has been improved by applying max(0.0, min(delay, 5.0)) after adding jitter. This ensures the delay is always within the safe bounded range of 0.0 to 5.0 seconds, regardless of the Retry-After value or jitter. This is a critical improvement for stability.

References
  1. Defensively validate and normalize user input even when it is expected to be valid from UI controls, as it can be malformed due to persisted data or external integrations.

Comment on lines +1563 to +1635
@pytest.mark.parametrize(
("retry_after", "now"),
[
("-10", None),
("nan", None),
("inf", None),
(
"Wed, 10 Dec 2025 12:00:00 GMT",
datetime.datetime(2025, 12, 10, 12, 0, 5, tzinfo=datetime.UTC),
),
],
)
def test_coordinator_retry_after_invalid_values_use_safe_default(
monkeypatch: pytest.MonkeyPatch,
retry_after: str,
now: datetime.datetime | None,
) -> None:
"""Invalid Retry-After values should fall back to a safe finite delay."""

session = SequenceSession(
[
ResponseSpec(
status=429,
payload={"error": {"message": "Quota exceeded"}},
headers={"Retry-After": retry_after},
),
ResponseSpec(
status=429,
payload={"error": {"message": "Quota exceeded"}},
headers={"Retry-After": retry_after},
),
]
)
delays: list[float] = []

async def _fast_sleep(delay: float) -> None:
assert isinstance(delay, float)
assert delay == delay
assert delay != float("inf")
assert delay != float("-inf")
delays.append(delay)

monkeypatch.setattr(client_mod.asyncio, "sleep", _fast_sleep)
monkeypatch.setattr(client_mod.random, "uniform", lambda *_args, **_kwargs: 0.0)
if now is not None:
monkeypatch.setattr(client_mod.dt_util, "utcnow", lambda: now)

client = client_mod.GooglePollenApiClient(session, "test")

loop = asyncio.new_event_loop()
hass = DummyHass(loop)
coordinator = coordinator_mod.PollenDataUpdateCoordinator(
hass=hass,
api_key="test",
lat=1.0,
lon=2.0,
hours=12,
language=None,
entry_id="entry",
forecast_days=1,
create_d1=False,
create_d2=False,
client=client,
)

try:
with pytest.raises(client_mod.UpdateFailed, match="Quota exceeded"):
loop.run_until_complete(coordinator._async_update_data())
finally:
loop.close()

assert session.calls == 2
assert delays == [2.0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new parameterized test test_coordinator_retry_after_invalid_values_use_safe_default provides excellent coverage for various invalid Retry-After values, including negative, NaN, infinity, and stale date-based values. This ensures that the system consistently falls back to a safe default delay, significantly improving the reliability of the backoff mechanism.

References
  1. Defensively validate and normalize user input even when it is expected to be valid from UI controls, as it can be malformed due to persisted data or external integrations.

Comment on lines 43 to +47
try:
return float(retry_after_raw)
parsed = float(retry_after_raw)
if math.isfinite(parsed) and parsed > 0:
return parsed
return 2.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _parse_retry_after function now correctly handles non-finite and negative float values for Retry-After by returning a default of 2.0. This is a good defensive programming practice to prevent unexpected behavior from malformed or malicious Retry-After headers.

References
  1. Defensively validate and normalize user input even when it is expected to be valid from UI controls, as it can be malformed due to persisted data or external integrations.

if retry_at is not None:
delay = (retry_at - dt_util.utcnow()).total_seconds()
if delay > 0:
if math.isfinite(delay) and delay > 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding math.isfinite(delay) to the condition for date-based Retry-After values ensures that infinite or NaN delays are also handled gracefully, preventing potential issues with asyncio.sleep.

References
  1. Defensively validate and normalize user input even when it is expected to be valid from UI controls, as it can be malformed due to persisted data or external integrations.

Comment on lines 60 to 62
r = _normalize_channel(color.get("red"))
g = _normalize_channel(color.get("green"))
b = _normalize_channel(color.get("blue"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The removal of the has_any_channel check simplifies the logic in _rgb_from_api. The _normalize_channel function is now solely responsible for validating and normalizing individual color channels, making the code cleaner and more focused.

Comment on lines +1096 to +1143
def test_coordinator_accepts_numeric_string_color_channels() -> None:
"""Numeric string channels should be normalized into RGB/hex values."""

payload = {
"dailyInfo": [
{
"date": {"year": 2025, "month": 7, "day": 1},
"pollenTypeInfo": [
{
"code": "GRASS",
"displayName": "Grass",
"indexInfo": {
"value": 1,
"category": "LOW",
"color": {"red": "1", "green": "0", "blue": "0"},
},
}
],
}
]
}

fake_session = FakeSession(payload)
client = client_mod.GooglePollenApiClient(fake_session, "test")

loop = asyncio.new_event_loop()
hass = DummyHass(loop)
coordinator = coordinator_mod.PollenDataUpdateCoordinator(
hass=hass,
api_key="test",
lat=1.0,
lon=2.0,
hours=12,
language=None,
entry_id="entry",
forecast_days=1,
create_d1=False,
create_d2=False,
client=client,
)

try:
data = loop.run_until_complete(coordinator._async_update_data())
finally:
loop.close()

assert data["type_grass"]["color_hex"] == "#FF0000"
assert data["type_grass"]["color_rgb"] == [255, 0, 0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This new test case test_coordinator_accepts_numeric_string_color_channels effectively verifies that numeric strings for color channels are correctly parsed and converted to RGB/hex values. This is important for robust handling of API payloads that might send numbers as strings.

References
  1. Defensively validate and normalize user input even when it is expected to be valid from UI controls, as it can be malformed due to persisted data or external integrations.

Comment on lines +1146 to +1193
def test_coordinator_ignores_invalid_string_color_channels() -> None:
"""Non-numeric string channels should not emit RGB/hex values."""

payload = {
"dailyInfo": [
{
"date": {"year": 2025, "month": 7, "day": 1},
"pollenTypeInfo": [
{
"code": "GRASS",
"displayName": "Grass",
"indexInfo": {
"value": 1,
"category": "LOW",
"color": {"red": "foo"},
},
}
],
}
]
}

fake_session = FakeSession(payload)
client = client_mod.GooglePollenApiClient(fake_session, "test")

loop = asyncio.new_event_loop()
hass = DummyHass(loop)
coordinator = coordinator_mod.PollenDataUpdateCoordinator(
hass=hass,
api_key="test",
lat=1.0,
lon=2.0,
hours=12,
language=None,
entry_id="entry",
forecast_days=1,
create_d1=False,
create_d2=False,
client=client,
)

try:
data = loop.run_until_complete(coordinator._async_update_data())
finally:
loop.close()

assert data["type_grass"]["color_hex"] is None
assert data["type_grass"]["color_rgb"] is None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test_coordinator_ignores_invalid_string_color_channels test case is well-designed to ensure that non-numeric strings in color channels are gracefully ignored, preventing crashes and ensuring None is returned for color_hex and color_rgb. This improves the robustness of the color parsing logic.

References
  1. Defensively validate and normalize user input even when it is expected to be valid from UI controls, as it can be malformed due to persisted data or external integrations.

@eXPerience83 eXPerience83 deleted the codex/verify-pollen-data-update-behavior branch February 23, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant